{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Creating Custom Feature Maps for Quantum Support Vector Machines\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Support vector machines (SVM) address the problem of supervised learning through the construction of a classifier. Havlicek *et al*. proposed two strategies to design a quantum SVM, namely the Quantum Kernel Estimator and the Quantum Variational Classifier. Both of these strategies use data that is provided classically and encodes it in the quantum state space through a quantum feature map [1]. The choice of which feature map to use is important and may depend on the given dataset we want to classify. In this tutorial, we show how to configure new feature maps in Aqua and explore their impact on the accuracy of the quantum classifier.\n",
    "\n",
    "Aqua provides several options for customizing the quantum feature map. In particular, there are four main parameters that can be used for model selection: the feature map circuit depth, the data map function for encoding the classical data, the quantum gate set and the order of expansion. We will go through each of these parameters in this tutorial, but before getting started, let us review the main concepts of the quantum feature map discussed in [1].\n",
    "\n",
    "[1] Havlicek _et al_.  Nature **567**, 209-212 (2019). https://www.nature.com/articles/s41586-019-0980-2, https://arxiv.org/abs/1804.11326\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Review of the Quantum Feature Map\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A quantum feature map nonlinearly maps a classical datum **x** to a quantum state $|\\Phi(\\mathbf{x})\\rangle\\langle\\Phi(\\mathbf{x})|$, a vector in the Hilbert space of density matrices. Support vector machine classifiers find a hyperplane separating each vector $|\\Phi(\\mathbf{x}_i)\\rangle\\langle\\Phi(\\mathbf{x}_i)|$ depending on its label, supported by a reduced amount of vectors (the so-called support vectors). A key element of the feature map is not only the use of quantum state space as a feature space but also the way data are mapped into this high dimensional space.\n",
    "\n",
    "Constructing feature maps based on quantum circuits that are hard to simulate classically is an important step towards obtaining a quantum advantage over classical approaches. The authors of [1] proposed a family of feature maps that is conjectured to be hard to simulate classically and that can be implemented as short-depth circuits on near-term quantum devices. The quantum feature map of depth $d$ is implemented by the unitary operator \n",
    "\n",
    "$$ \\mathcal{U}_{\\Phi(\\mathbf{x})}=\\prod_d U_{\\Phi(\\mathbf{x})}H^{\\otimes n},\\ U_{\\Phi(\\mathbf{x})}=\\exp\\left(i\\sum_{S\\subseteq[n]}\\phi_S(\\mathbf{x})\\prod_{k\\in S} P_k\\right), $$\n",
    "\n",
    "which contains layers of Hadamard gates interleaved with entangling blocks encoding the classical data as shown in circuit diagram below for $d=2$.\n",
    "\n",
    "<img src=\"images/uphi.PNG\" width=\"400\" />\n",
    "\n",
    "The number of qubits $n$ in the quantum circuit is equal to the dimensionality of the classical data $\\mathbf{x}$, which are encoded through the coefficients $\\phi_S(\\mathbf{x})$, where $S \\subseteq[n] = \\{1, \\ldots, n \\}$. We call the $r$-th order expansion the feature map of this circuit family when $|S|\\leq r$. In Aqua, the default is the second order expansion $|S|\\leq 2$ used in [1], which gives $n$ singeltons $S=\\{i\\}$ and, depending on the connectivity graph of the quantum device, up to $\\frac{n(n-1)}{2}$ couples to encode non-linear interactions. The greater the upper bound $r$, the more interactions will be taken into account.\n",
    "\n",
    "Only contributions from $Z$ and $ZZ$ gates in the entangling blocks are considered in [1]. In general, the blocks can be expressed in terms of the Pauli gates $P_k \\in \\{\\mathbb{1}_k, X_k, Y_k, Z_k \\}$.\n",
    "\n",
    "In Aqua, the circuit depth $d$, coefficients $\\phi_S$, expansion order $r$, and gates $P_k$ are mutable for both classification algorithms (Quantum Variational Classifier and Quantum Kernel Estimator). As discussed in [1], the depth $d=1$ circuit can be efficiently simulated classically by uniform sampling, while the $d=2$ variant is conjectured to be hard to simulate classically."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Programming the Quantum Feature Map\n",
    "\n",
    "We will now see how to configure quantum feature maps in Aqua by modifing the circuit depth $d$, data map function $\\phi_S$, expansion order $r$, and gates $P_k$. Documentation on the quantum feature maps in Aqua can be found at https://qiskit.org/documentation/aqua/feature_maps.html. To configure and compare different feature maps, we will use synthetic data from `datasets.py`, which is generated by the `SecondOrderExpansion` feature map with default settings. As a result, we expect high classification accuracy when training the model with this same feature map. \n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "scrolled": false
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "import functools\n",
    "\n",
    "from qiskit import BasicAer\n",
    "from qiskit.circuit.library import ZZFeatureMap, PauliFeatureMap\n",
    "from qiskit.aqua import QuantumInstance\n",
    "from qiskit.aqua.algorithms import QSVM\n",
    "from qiskit.ml.datasets import ad_hoc_data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Generate synthetic training and test sets from the SecondOrderExpansion quantum feature map\n",
    "feature_dim = 2\n",
    "sample_Total, training_dataset, test_dataset, class_labels = ad_hoc_data(training_size=10, \n",
    "                                                                         test_size=5,\n",
    "                                                                         n=feature_dim, \n",
    "                                                                         gap=0.3)\n",
    "\n",
    "# Using the statevector simulator\n",
    "backend = BasicAer.get_backend('statevector_simulator')\n",
    "random_seed = 10598\n",
    "\n",
    "quantum_instance = QuantumInstance(backend, shots=1024, seed_simulator=random_seed, seed_transpiler=random_seed)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "With this synthetic data, we will use the Quantum Kernel Estimator to test different feature maps, starting with a first order expansion of the feature map discussed in [1]. From there, we will explore more complex feature maps with higher order expansions and custom functions to map the classical data."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 1. First Order Diagonal Expansion\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A first order diagonal expansion is implemented using the `ZFeatureMap` feature map where $|S|=1$. The resulting circuit contains no interactions between features of the encoded data, and therefore no entanglement. The feature map can take the following inputs:\n",
    "\n",
    "- `feature_dimension`: dimensionality of the classical data (equal to the number of required qubits)\n",
    "- `reps`: number of times $d$ to repeat the feature map circuit \n",
    "- `data_map_func`: function $\\phi_S(\\mathbf{x})$ encoding the classical data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "testing success ratio:  1.0\n"
     ]
    }
   ],
   "source": [
    "# Generate the feature map\n",
    "feature_map = ZZFeatureMap(feature_dimension=feature_dim, reps=2, entanglement='linear')\n",
    "\n",
    "# Run the Quantum Kernel Estimator and classify the test data\n",
    "qsvm = QSVM(feature_map=feature_map, training_dataset=training_dataset, test_dataset=test_dataset)\n",
    "\n",
    "result = qsvm.run(quantum_instance)\n",
    "print(\"testing success ratio: \", result['testing_accuracy'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We see that the first order expansion feature map yields poor classification accuracy on data generated to be separable by the second order expansion."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 2. Second Order Diagonal Expansion\n",
    "\n",
    "The `ZZFeatureMap` feature map allows $|S|\\leq 2$, so interactions in the data will be encoded in the feature map according to the connectivity graph and the classical data map. `ZZFeatureMap` with default parameters is equivalent to the feature map described in [1] and can take the additional inputs:\n",
    "\n",
    "- `entangler_map`: encodes qubit connectivity (default `None` uses a precomputed connectivity graph according to `entanglement`) \n",
    "- `entanglement`: generates connectivity `'full'` or `'linear'` if `entangler_map` not provided (default value `'full'` indicates a complete connectivity graph of $\\frac{n(n-1)}{2}$ interactions)\n",
    "\n",
    "The default setting for `data_map_func` in `ZZFeatureMap` is given by\n",
    "\n",
    "$$\\phi_S:x\\mapsto \\Bigg\\{\\begin{array}{ll}\n",
    "    x_i & \\mbox{if}\\ S=\\{i\\} \\\\\n",
    "        (\\pi-x_i)(\\pi-x_j) & \\mbox{if}\\ S=\\{i,j\\}\n",
    "    \\end{array}$$.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "testing success ratio:  1.0\n"
     ]
    }
   ],
   "source": [
    "feature_map = ZZFeatureMap(feature_dimension=feature_dim, reps=2)\n",
    "\n",
    "qsvm = QSVM(feature_map=feature_map, training_dataset=training_dataset, test_dataset=test_dataset)\n",
    "\n",
    "result = qsvm.run(quantum_instance)\n",
    "print(\"testing success ratio: \", result['testing_accuracy'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As expected, the second order feature map yields high test accuracy on this dataset."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 3. Second Order Diagonal Expansion with Custom Data Map\n",
    "\n",
    "Instead of using the default data map $\\phi_S(\\mathbf{x})$ in Aqua, we can encode the classical data using custom functions. For example, we will create the following map for our data (shown for $|S| \\le 2$, but defined similarly for higher order terms):\n",
    "\n",
    "$$\\phi_S:x\\mapsto \\Bigg\\{\\begin{array}{ll}\n",
    "    x_i & \\mbox{if}\\ S=\\{i\\} \\\\\n",
    "        \\sin(\\pi-x_i)\\sin(\\pi-x_j) & \\mbox{if}\\ S=\\{i,j\\}\n",
    "    \\end{array}$$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "def custom_data_map_func(x):\n",
    "    \"\"\"Define a function map from R^n to R.\n",
    "    \n",
    "    Args:\n",
    "        x (np.ndarray): data\n",
    "    Returns:\n",
    "        double: the mapped value\n",
    "    \"\"\"\n",
    "    coeff = x[0] if len(x) == 1 else functools.reduce(lambda m, n: m * n, np.pi - x)\n",
    "    return coeff"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let us now test this custom data map on the synthetic dataset."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "testing success ratio:  1.0\n"
     ]
    }
   ],
   "source": [
    "feature_map = ZZFeatureMap(feature_dimension=feature_dim, reps=2, data_map_func=custom_data_map_func)\n",
    "\n",
    "qsvm = QSVM(feature_map=feature_map, training_dataset=training_dataset, test_dataset=test_dataset)\n",
    "\n",
    "result = qsvm.run(quantum_instance)\n",
    "print(\"testing success ratio: \", result['testing_accuracy'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We see that this choice for the data map function reduced the accuracy of the model."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 4. Second Order Pauli Expansion\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For some applications, we may want to consider a more general form of the feature map. One way to generalize is to use `PauliFeatureMap` and specify a set of Pauli gates instead of using the default $Z$ gates. This feature map has the same parameters as `ZFeatureMap` and `ZZFeatureMap` such as `reps` and `data_map_function` along with an additional `paulis` parameter to change the gate set. This parameter is a list of strings, each representing the desired Pauli gate(s). The default value is `['Z', 'ZZ']`, which is equivalent to `ZZFeatureMap`.\n",
    "\n",
    "\n",
    "Each string in `paulis` is implemented one at a time for each layer in the depth $d$ feature map circuit. A single character, for example `'Z'`, adds one layer of the corresponding single-qubit gates, while terms such as `'ZZ'` or `'XY'` add a layer of corresponding two-qubit entangling gates for each qubit pair available.\n",
    "\n",
    "For example, the choice `paulis = ['Z', 'Y', 'ZZ']` generates a quantum feature map of the form \n",
    "\n",
    "$$\\mathcal{U}_{\\Phi(\\mathbf{x})} = \\left( \\exp\\left(i\\sum_{jk} \\phi_{\\{j,k\\}}(\\mathbf{x}) \\, Z_j \\otimes Z_k\\right) \\, \\exp\\left(i\\sum_{j} \\phi_{\\{j\\}}(\\mathbf{x}) \\, Y_j\\right) \\, \\exp\\left(i\\sum_j \\phi_{\\{j\\}}(\\mathbf{x}) \\, Z_j\\right) \\, H^{\\otimes n} \\right)^d.$$ \n",
    "\n",
    "The depth $d=1$ version of this quantum circuit is shown in the figure below for $n=2$ qubits.\n",
    "\n",
    "<br>\n",
    "<img src=\"images/depth1.PNG\" width=\"400\"/>\n",
    "<br>\n",
    "\n",
    "The circuit begins with a layer of Hadamard gates $H^{\\otimes n}$ followed by a layer of single-qubit $A = e^{i\\phi_{\\{j\\}}(\\mathbf{x})Z_j}$ gates and a layer of $B = e^{i\\phi_{\\{j\\}}(\\mathbf{x}) \\, Y_j}$ gates. The $A$ and $B$ gates are parametrized by the same set of angles $\\phi_{\\{j\\}}(\\mathbf{x})$ but around different axes. The diagonal entangling gate $e^{i \\phi_{\\{0,1\\}}(\\mathbf{x}) \\, Z_0 \\otimes Z_1}$ is parametrized by an angle $\\phi_{\\{0,1\\}}(\\mathbf{x})$ and can be implemented using two controlled-NOT gates and one $A'=e^{i\\phi_{\\{0,1\\}}(x)\\, Z_1}$ gate as shown in the figure.\n",
    "\n",
    "As a comparison, `paulis = ['Z', 'ZZ']` creates the same circuit as above but without the $B$ gates, while `paulis = ['Z', 'YY']` creates a circuit with a layer of $A$ gates followed by a layer of entangling gates $e^{i \\phi_{\\{0,1\\}}(\\mathbf{x}) \\, Y_0 \\otimes Y_1}$.\n",
    "\n",
    "Below, we test the `PauliFeatureMap` with `paulis=['Z', 'Y', 'ZZ']`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "testing success ratio:  0.5\n"
     ]
    }
   ],
   "source": [
    "feature_map = PauliFeatureMap(feature_dimension=feature_dim, reps=2, paulis = ['Z','Y','ZZ'])\n",
    "\n",
    "qsvm = QSVM(feature_map=feature_map, training_dataset=training_dataset, test_dataset=test_dataset)\n",
    "\n",
    "result = qsvm.run(quantum_instance)\n",
    "print(\"testing success ratio: \", result['testing_accuracy'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 5. Third Order Pauli Expansion with Custom Data Map\n",
    "\n",
    "Third order or higher expansions can be configured using `PauliExpansion`. For example, assuming the classical data has dimensionality of at least three and we have access to three qubits, `paulis = ['Y', 'Z', 'ZZ', 'ZZZ']` generates a feature map according to the previously mentioned rule, with $|S|\\leq 3$. \n",
    "\n",
    "Suppose we want to classify data with three features using a third order expansion, a custom data map, and a circuit of depth $d=2$. We can do this with the following code in Aqua."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "feature_dim = 3\n",
    "sample_Total_b, training_dataset_b, test_dataset_b, class_labels = ad_hoc_data(training_size=20, test_size=10, \n",
    "                                                                               n=feature_dim, gap=0.3, \n",
    "                                                                               plot_data=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "testing success ratio:  0.45\n"
     ]
    }
   ],
   "source": [
    "feature_map = PauliFeatureMap(feature_dimension=feature_dim, reps=2, \n",
    "                              paulis = ['Y','Z','ZZ','ZZZ'], data_map_func=custom_data_map_func)\n",
    "\n",
    "qsvm = QSVM(feature_map=feature_map, training_dataset=training_dataset_b, test_dataset=test_dataset_b)\n",
    "\n",
    "result = qsvm.run(quantum_instance)\n",
    "print(\"testing success ratio: \", result['testing_accuracy'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The qubit connectivity is `'full'` by default, so each layer of this depth $d=2$ circuit will contain the sequence: \n",
    "\n",
    "- One layer of $B = e^{i\\phi_{\\{j\\}}(\\mathbf{x})\\,Y_j}$ gates followed by one layer of $A = e^{i\\phi_{\\{j\\}}(\\mathbf{x})\\,Z_j}$ gates\n",
    "- One layer containing a $ZZ$ entangler $e^{i \\phi_{\\{j,k\\}}(\\mathbf{x}) \\,Z_j \\otimes Z_k}$ for each pair of qubits $(0,1),\\ (1,2),\\ (0,2)$\n",
    "- One layer containing a $ZZZ$ entangler $e^{i\\phi_{\\{0,1,2 \\}}(x)\\,Z_0 \\otimes Z_1 \\otimes Z_2}$ where $\\phi_{\\{jkl\\}} = \\sin(\\pi-x_j)\\sin(\\pi-x_k)\\sin(\\pi-x_l)$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Building New Feature Maps\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this tutorial, we have seen how to generate feature maps from the circuit family described in [1]. To explore new circuit families, we can create a new class implementing the class `FeatureMap`, and its method `construct_circuit`, and the new feature map will be pluggable in any Aqua component requiring a feature map. More information on adding new feature maps can be found in the documentation https://qiskit.org/documentation/aqua/feature_maps.html.\n",
    "\n",
    "As an example to illustrate the process, below we show a general custom feature map class with the circuit construction method that creates a quantum circuit consisting of successive layers of $R_X$ gates and $ZZ$ gates."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "from qiskit.aqua.components.feature_maps import FeatureMap\n",
    "from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister\n",
    "from qiskit.circuit.library import BlueprintCircuit\n",
    "\n",
    "class CustomFeatureMap(FeatureMap):\n",
    "    \"\"\"Mapping data with a custom feature map.\"\"\"\n",
    "    \n",
    "    def __init__(self, feature_dimension, depth=2, entangler_map=None):\n",
    "        \"\"\"\n",
    "        Args:\n",
    "            feature_dimension (int): number of features\n",
    "            depth (int): the number of repeated circuits\n",
    "            entangler_map (list[list]): describe the connectivity of qubits, each list describes\n",
    "                                        [source, target], or None for full entanglement.\n",
    "                                        Note that the order is the list is the order of\n",
    "                                        applying the two-qubit gate.        \n",
    "        \"\"\"\n",
    "        self._support_parameterized_circuit = False\n",
    "        self._feature_dimension = feature_dimension\n",
    "        self._num_qubits = self._feature_dimension = feature_dimension\n",
    "        self._depth = depth\n",
    "        self._entangler_map = None\n",
    "        if self._entangler_map is None:\n",
    "            self._entangler_map = [[i, j] for i in range(self._feature_dimension) for j in range(i + 1, self._feature_dimension)]\n",
    "            \n",
    "    def construct_circuit(self, x, qr, inverse=False):\n",
    "        \"\"\"Construct the feature map circuit.\n",
    "        \n",
    "        Args:\n",
    "            x (numpy.ndarray): 1-D to-be-transformed data.\n",
    "            qr (QauntumRegister): the QuantumRegister object for the circuit.\n",
    "            inverse (bool): whether or not to invert the circuit.\n",
    "            \n",
    "        Returns:\n",
    "            QuantumCircuit: a quantum circuit transforming data x.\n",
    "        \"\"\"\n",
    "        qc = QuantumCircuit(qr)\n",
    "\n",
    "        \n",
    "        for _ in range(self._depth):\n",
    "            for i in range(self._feature_dimension):\n",
    "                qc.rx(x[i], qr[i])\n",
    "            for [source, target] in self._entangler_map:\n",
    "                qc.cx(qr[source], qr[target])\n",
    "                qc.u1(x[source] * x[target], qr[target])\n",
    "                qc.cx(qr[source], qr[target])\n",
    "                    \n",
    "        if inverse:\n",
    "            qc.inverse()\n",
    "        return qc"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/Users/manoel/anaconda3/envs/Qiskitenv/lib/python3.7/site-packages/ipykernel_launcher.py:3: DeprecationWarning: \n",
      "                The <class '__main__.CustomFeatureMap'> object as input for the QSVM is deprecated as of 0.7.0 and will\n",
      "                be removed no earlier than 3 months after the release.\n",
      "                You should pass a QuantumCircuit object instead.\n",
      "                See also qiskit.circuit.library.data_preparation for a collection\n",
      "                of suitable circuits.\n",
      "  This is separate from the ipykernel package so we can avoid doing imports until\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "testing success ratio:  0.6\n"
     ]
    }
   ],
   "source": [
    "feature_map = CustomFeatureMap(feature_dimension=2, depth=2)\n",
    "\n",
    "qsvm = QSVM(feature_map=feature_map, training_dataset=training_dataset, test_dataset=test_dataset)\n",
    "\n",
    "result = qsvm.run(quantum_instance)\n",
    "print(\"testing success ratio: \", result['testing_accuracy'])"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
